Components
Components are the building blocks of the user interface. Declare a component with the component keyword followed by its name and body.
Declaration
import { Text } from ui
component Welcome {
Text("Hello, world!")
}
A component body can produce multiple sibling components. When such a component is placed inside a container like VStack, all produced components appear as direct siblings within that container — no intermediate wrapper is inserted. This means container properties like spacing apply to each of them equally.
import { VStack, Text } from ui
component Group {
@property include: Bool
Text("First")
Text("Second")
if self.include {
Text("Third")
}
}
// All three texts become direct children of VStack,
// each separated by its spacing of 8.
export var main: Component = VStack(spacing: 8) {
Group(include: true)
}
Declarative if and for in component bodies are covered in Declarative Control Flow.
Properties
Declare inputs with @property. Properties allow data to flow from the parent into the component and are read-only inside the component.
component Greeting {
@property name: String
Text("Hello, \{self.name}!")
}
Properties are passed as named arguments at the call site:
Greeting(name: "Alice")
Callback Properties
A property of function type acts as a callback, allowing information to flow back up to the parent:
import { Text, TapGesture, TapGestureEvent } from ui
component Button {
@property label: String
@property on_press: (Bool) -> ()
Text(self.label) with {
TapGesture(fun (event: TapGestureEvent) {
self.on_press(event.modifiers.shift)
})
}
}
Bindings
A binding is a read-write reference to a state value or class property. It allows a child component to both read and mutate a value owned by someone else — without receiving a copy of it.
Declaring a Binding Input
Use @binding to declare that a component or modifier receives a binding rather than a plain value. Inside the component, the binding behaves like a normal read-write value:
component Checkbox {
@binding checked: Bool
Rectangle(color: self.checked ? Color(0xFFFFFFFF) : Color(0x66FFFFFF)) with {
TapGesture(fun (_) {
self.checked = not self.checked // writes back to the owner's state
})
}
}
Passing a Binding
Obtain a binding by prefixing a state name or class property name with $ when passing it. This works for stored state, read-write computed state, and class properties:
component Main {
checked: Bool = false
inverted_checked: Bool {
get { return not self.checked }
set(v) { self.checked = not v }
}
Checkbox(checked: self.$checked) // stored state
Checkbox(checked: self.$inverted_checked) // read-write computed state
}
Class properties can be passed as bindings the same way:
var model = Model()
Checkbox(checked: model.$active)
A component that receives a binding can forward it to a child component using the same $ prefix on its own binding name:
component LabeledCheckbox {
@property label: String
@binding checked: Bool
HStack {
Text(self.label)
Checkbox(checked: self.$checked) // forwards the binding onwards
}
}
The Binding Type
The $ accessor returns a value of the generic binding type $<T>, for example $<Bool>. This type can be used explicitly in a constructor signature, which is necessary when a component accepts a binding and defines a custom constructor:
component Checkbox {
@binding checked: Bool
constructor(label: String, checked: $<Bool>) : (
checked: checked,
)
// ...
}
Bindings are opaque — they cannot be read, set, or created directly in code. The only way to obtain one is via a $-prefixed state or property access, and the only way to read or write through one is via a @binding declaration inside a component or modifier.
Constructors
Like classes, components have a generated default constructor that accepts all @property and @binding declarations as named arguments. A custom constructor can be defined with the constructor keyword to provide an alternative call signature, delegating to the default constructor via : (…).
Constructors follow the same parameter rules as functions: parameters can have custom argument labels, be made positional with _, or be given default values. See Functions for the full parameter reference.
component Card {
@property title: String
@property width: Float
@property height: Float
constructor(title: String, size: Float) : (
title: title,
width: size,
height: size,
)
Text(self.title)
}
export var main: Component = Card(title: "Hello", size: 100.0)
Unlike class constructors, component constructors cannot have a body block.
Multiple constructors can be defined, as long as each has a distinct parameter signature:
import { VStack, Text } from ui
component Card {
@property title: String
@property width: Float
@property height: Float
constructor(title: String, size: Float) : (
title: title,
width: size,
height: size,
)
constructor(title: String, width: Float, height: Float) : (
title: title,
width: width,
height: height,
)
Text(self.title)
}
export var main: Component = VStack {
Card(title: "Square", size: 100.0)
Card(title: "Banner", width: 200.0, height: 50.0)
}
State
State is internal mutable data owned by the component. It cannot be set from outside. Declare state with a name, type, and initial value.
import { Text, TapGesture, TapGestureEvent } from ui
component Counter {
count: Int = 0
Text("\{self.count}") with {
TapGesture(fun (_: TapGestureEvent) {
self.count = self.count + 1
})
}
}
When a state value changes, every expression that depends on it is re-evaluated automatically.
Computed State
Computed state derives its value from other state and is re-evaluated automatically when one of its dependencies changes.
Read-only
A shorthand body written directly after the type creates a read-only computed state:
component Toggle {
on: Bool = false
label: String {
return self.on ? "On" : "Off"
}
Text(self.label)
}
Read-write
Use explicit get and set blocks to make a computed state writable:
component Slider {
raw_value: Float = 0.0
percentage: Float {
get { return self.raw_value * 100.0 }
set(p) { self.raw_value = p / 100.0 }
}
Text("\{self.percentage}%")
}
Children
A component can accept any number of @property declarations of type Template(…). The special case is a property named children with type Template() — this is what enables the trailing block syntax at the call site.
import { HStack, Text } from ui
component Row {
@property children: Template()
HStack(spacing: 8) {
self.children()
}
}
// The "children: Template()" property enables the trailing block
export var main: Component = Row {
Text("First")
Text("Second")
}
When using the trailing block syntax, the parentheses are optional as long as there are no other arguments. All three of these are equivalent:
VStack(spacing: 8) { … } // other arguments present — parentheses required
VStack() { … } // no arguments — empty parentheses optional
VStack { … } // no arguments — parentheses omitted entirely
Any other template properties — whether named differently or with a parameterized Template(…) signature — must be passed explicitly as named arguments:
import { VStack, HStack, Text } from ui
component Layout {
@property children: Template() // trailing block
@property header: Template() // must be passed explicitly
@property item: Template(String) // must be passed explicitly
VStack {
self.header()
self.item("Featured")
HStack {
self.children()
}
}
}
export var main: Component = Layout(
header: template () { Text("Title") },
item: template (text) { Text(text) },
) {
Text("First")
Text("Second")
}
self
Inside a component, self refers to the component instance and provides access to all its properties, state, bindings, and methods.
Exporting
A component can be exported from a module. The entry point of a Kontakt instrument is an exported main variable of type Component.
import { Text } from ui
component Welcome {
Text("Hello, world!")
}
export var main: Component = Welcome()
See Modules for details on exports.